/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (C) 1998-2005 Julian Hyde
// Copyright (C) 2005-2021 Hitachi Vantara and others
// All Rights Reserved.
*/
package mondrian.olap;

import mondrian.calc.*;
import mondrian.mdx.*;
import mondrian.olap.fun.ParameterFunDef;
import mondrian.olap.type.*;
import mondrian.resource.MondrianResource;
import mondrian.rolap.*;
import mondrian.server.*;
import mondrian.spi.ProfileHandler;
import mondrian.util.ArrayStack;

import org.apache.commons.collections.collection.CompositeCollection;

import org.olap4j.impl.*;
import org.olap4j.mdx.IdentifierSegment;

import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.*;

/**
 * <code>Query</code> is an MDX query.
 *
 * <p>
 * It is created by calling {@link Connection#parseQuery}, and executed by calling {@link Connection#execute}, to return
 * a {@link Result}.
 * </p>
 *
 * <h3>Query control</h3>
 *
 * <p>
 * Most queries are model citizens, executing quickly (often using cached results from previous queries), but som
 * queries take more time, or more database resources, or more results, than is reasonable. Mondrian offers three ways
 * to control rogue queries:
 * <ul>
 *
 * <li>You can set a query timeout by setting the {@link MondrianProperties#QueryTimeout} parameter. If the query takes
 * longer to execute than the value of this parameter, the system will kill it.</li>
 *
 * <li>The {@link MondrianProperties#QueryLimit} parameter limits the number of cells returned by a query.</li>
 *
 * <li>At any time while a query is executing, another thread can cancel the query by calling {@link #getStatement()}.
 * {@link Statement#cancel() cancel()}. The call to {@link Connection#execute(Query)} will throw an exception.</li>
 *
 * </ul>
 *
 * @author jhyde, 20 January, 1999
 */
public class Query extends QueryPart {

  private Formula[] formulas;

  /**
   * public-private: This must be public because it is still accessed in rolap.RolapConnection
   */
  public QueryAxis[] axes;

  private QueryAxis slicerAxis;

  /**
   * Definitions of all parameters used in this query.
   */
  private final List<Parameter> parameters = new ArrayList<>();

  private final Map<String, Parameter> parametersByName = new HashMap<>();

  /**
   * Cell properties. Not currently used.
   */
  private final QueryPart[] cellProps;

  /**
   * Cube this query belongs to.
   */
  private final Cube cube;

  private final Statement statement;
  public Calc[] axisCalcs;
  public Calc slicerCalc;

  /**
   * Set of FunDefs for which alerts about non-native evaluation have already been posted.
   */
  Set<FunDef> alertedNonNativeFunDefs;

  /**
   * Unique list of members referenced from the measures dimension. Will be used to determine if cross joins can be
   * processed natively for virtual cubes.
   */
  private Set<Member> measuresMembers;

  /**
   * If true, virtual cubes can be processed using native cross joins. It defaults to true, unless functions are applied
   * on measures.
   */
  private boolean nativeCrossJoinVirtualCube;

  /**
   * Used for virtual cubes. Comtains a list of base cubes related to a virtual cube
   */
  private List<RolapCube> baseCubes;

  /**
   * If true, enforce validation even when ignoreInvalidMembers is set.
   */
  private boolean strictValidation;

  /**
   * How should the query be returned? Valid values are: ResultStyle.ITERABLE ResultStyle.LIST ResultStyle.MUTABLE_LIST
   * For java4, use LIST
   */
  private ResultStyle resultStyle = Util.Retrowoven ? ResultStyle.LIST : ResultStyle.ITERABLE;

  private Map<String, Object> evalCache = new HashMap<String, Object>();

  /**
   * List of aliased expressions defined in this query, and where they are defined. There might be more than one aliased
   * expression with the same name.
   */
  private final List<ScopedNamedSet> scopedNamedSets = new ArrayList<>();
  private boolean ownStatement;

  /**
   * Creates a Query.
   */
  public Query( Statement statement, Formula[] formulas, QueryAxis[] axes, String cube, QueryAxis slicerAxis,
      QueryPart[] cellProps, boolean strictValidation ) {
    this( statement, Util.lookupCube( statement.getSchemaReader(), cube, true ), formulas, axes, slicerAxis, cellProps,
        new Parameter[0], strictValidation );
  }

  /**
   * Creates a Query.
   */
  public Query( Statement statement, Cube mdxCube, Formula[] formulas, QueryAxis[] axes, QueryAxis slicerAxis,
      QueryPart[] cellProps, Parameter[] parameters, boolean strictValidation ) {
    this.statement = statement;
    this.cube = mdxCube;
    this.formulas = formulas;
    this.axes = axes;
    normalizeAxes();
    this.slicerAxis = slicerAxis;
    this.cellProps = cellProps;
    this.parameters.addAll( Arrays.asList( parameters ) );
    this.measuresMembers = new HashSet<Member>();
    // assume, for now, that cross joins on virtual cubes can be
    // processed natively; as we parse the query, we'll know otherwise
    this.nativeCrossJoinVirtualCube = true;
    this.strictValidation = strictValidation;
    this.alertedNonNativeFunDefs = new HashSet<FunDef>();
    statement.setQuery( this );

    if ( RolapUtil.PROFILE_LOGGER.isDebugEnabled() && statement.getProfileHandler() == null ) {
      statement.enableProfiling( new ProfileHandler() {
        public void explain( String plan, QueryTiming timing ) {
          if ( timing != null ) {
            plan += "\n" + timing;
          }
          RolapUtil.PROFILE_LOGGER.debug( plan );
        }
      } );
    }
    resolve();
  }

  /**
   * Sets the timeout in milliseconds of this Query.
   *
   * <p>
   * Zero means no timeout.
   *
   * @param queryTimeoutMillis
   *          Timeout in milliseconds
   *
   * @deprecated This method will be removed in mondrian-4.0
   */
  public void setQueryTimeoutMillis( long queryTimeoutMillis ) {
    statement.setQueryTimeoutMillis( queryTimeoutMillis );
  }

  /**
   * Checks whether the property name is present in the query.
   */
  public boolean hasCellProperty( String propertyName ) {
    for ( QueryPart cellProp : cellProps ) {
      if ( ( (CellProperty) cellProp ).isNameEquals( propertyName ) ) {
        return true;
      }
    }
    return false;
  }

  /**
   * Checks whether any cell property present in the query
   */
  public boolean isCellPropertyEmpty() {
    return cellProps.length == 0;
  }

  /**
   * Adds a new formula specifying a set to an existing query.
   */
  public void addFormula( Id id, Exp exp ) {
    addFormula( new Formula( false, id, exp, new MemberProperty[0], null, null ) );
  }

  /**
   * Adds a new formula specifying a member to an existing query.
   *
   * @param id
   *          Name of member
   * @param exp
   *          Expression for member
   * @param memberProperties
   *          Properties of member
   */
  public void addFormula( Id id, Exp exp, MemberProperty[] memberProperties ) {
    addFormula( new Formula( true, id, exp, memberProperties, null, null ) );
  }

  /**
   * Adds a new formula specifying a member or a set to an existing query; resolve is called after the formula has been
   * added.
   *
   * @param formula
   *          Formula to add to query
   */
  public void addFormula( Formula formula ) {
    formulas = Util.append( formulas, formula );
    resolve();
  }

  /**
   * Adds some number of new formulas specifying members or sets to an existing query; resolve is only called once,
   * after all the new members have been added to the query.
   *
   * @param additions
   *          Formulas to add to query
   */
  public void addFormulas( Formula... additions ) {
    formulas = Util.appendArrays( formulas, additions );
    resolve();
  }

  /**
   * Creates a validator for this query.
   *
   * @return Validator
   */
  public Validator createValidator() {
    return createValidator( statement.getSchema().getFunTable(), false, new HashMap<>() );
  }

  public Validator createValidator( Map<QueryPart, QueryPart> resolvedIdentifiers ) {
    return createValidator( statement.getSchema().getFunTable(), false, resolvedIdentifiers );
  }

  /**
   * Creates a validator for this query that uses a given function table and function validation policy.
   *
   * @param functionTable
   *          Function table
   * @param alwaysResolveFunDef
   *          Whether to always resolve function definitions (see {@link Validator#alwaysResolveFunDef()})
   * @return Validator
   */
  public Validator createValidator( FunTable functionTable, boolean alwaysResolveFunDef ) {
    return new QueryValidator( functionTable, alwaysResolveFunDef, Query.this, new HashMap<>() );
  }

  public Validator createValidator( FunTable functionTable, boolean alwaysResolveFunDef,
      Map<QueryPart, QueryPart> resolvedIdentifiers ) {
    return new QueryValidator( functionTable, alwaysResolveFunDef, Query.this, resolvedIdentifiers );
  }

  /**
   * @deprecated Please use {@link #clone}; this method will be removed in mondrian-4.0
   */
  public Query safeClone() {
    return clone();
  }

  @SuppressWarnings( { "CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException" } )
  public Query clone() {
    return new Query( statement, cube, Formula.cloneArray( formulas ), QueryAxis.cloneArray( axes ),
        ( slicerAxis == null ) ? null : (QueryAxis) slicerAxis.clone(), cellProps, parameters.toArray(
            new Parameter[parameters.size()] ), strictValidation );
  }

  public Connection getConnection() {
    return statement.getMondrianConnection();
  }

  /**
   * Issues a cancel request on this Query object. Once the thread running the query detects the cancel request, the
   * query execution will throw an exception. See <code>BasicQueryTest.testCancel</code> for an example of usage of this
   * method.
   *
   * @deprecated This method is deprecated and will be removed in mondrian-4.0
   */
  public void cancel() {
    try {
      statement.cancel();
    } catch ( SQLException e ) {
      throw new RuntimeException( e );
    }
  }

  /**
   * Checks if either a cancel request has been issued on the query or the execution time has exceeded the timeout value
   * (if one has been set). Exceptions are raised if either of these two conditions are met. This method should be
   * called periodically during query execution to ensure timely detection of these events, particularly before/after
   * any potentially long running operations.
   *
   * @deprecated This method will be removed in mondrian-4.0
   */
  public void checkCancelOrTimeout() {
    final Execution execution0 = statement.getCurrentExecution();
    if ( execution0 == null ) {
      return;
    }
    execution0.checkCancelOrTimeout();
  }

  /**
   * Gets the query start time
   * 
   * @return start time
   *
   * @deprecated Use {@link Execution#getStartTime}. This method is deprecated and will be removed in mondrian-4.0
   */
  public long getQueryStartTime() {
    final Execution currentExecution = statement.getCurrentExecution();
    return currentExecution == null ? 0 : currentExecution.getStartTime();
  }

  /**
   * Determines whether an alert for non-native evaluation needs to be posted.
   *
   * @param funDef
   *          function type to alert for
   *
   * @return true if alert should be raised
   */
  public boolean shouldAlertForNonNative( FunDef funDef ) {
    return alertedNonNativeFunDefs.add( funDef );
  }

  private void normalizeAxes() {
    for ( int i = 0; i < axes.length; i++ ) {
      AxisOrdinal correctOrdinal = AxisOrdinal.StandardAxisOrdinal.forLogicalOrdinal( i );
      if ( axes[i].getAxisOrdinal() != correctOrdinal ) {
        for ( int j = i + 1; j < axes.length; j++ ) {
          if ( axes[j].getAxisOrdinal() == correctOrdinal ) {
            // swap axes
            QueryAxis temp = axes[i];
            axes[i] = axes[j];
            axes[j] = temp;
            break;
          }
        }
      }
    }
  }

  /**
   * Performs type-checking and validates internal consistency of a query, using the default resolver.
   *
   * <p>
   * This method is called automatically when a query is created; you need to call this method manually if you have
   * modified the query's expression tree in any way.
   */
  public void resolve() {
    // Before commencing validation, create all calculated members
    // and calculated sets
    createFormulaElements();
    Map<QueryPart, QueryPart> resolvedIdentifiers = new IdBatchResolver( this ).resolve();
    final Validator validator = createValidator( resolvedIdentifiers );
    resolve( validator ); // resolve self and children
    // Create a dummy result so we can use its evaluator
    final Evaluator evaluator = RolapUtil.createEvaluator( statement );
    ExpCompiler compiler = createCompiler( evaluator, validator, Collections.singletonList( resultStyle ) );
    compile( compiler );
  }

  private void createFormulaElements() {
    if ( formulas != null ) {
      // Resolving of formulas should be done in two parts
      // because formulas might depend on each other, so all calculated
      // mdx elements have to be defined during resolve.
      for ( Formula formula : formulas ) {
        formula.createElement( this );
      }
    }
  }

  /**
   * @return true if the relevant property for ignoring invalid members is set to true for this query's environment (a
   *         different property is checked depending on whether environment is schema load vs query validation)
   */
  public boolean ignoreInvalidMembers() {
    MondrianProperties props = MondrianProperties.instance();
    final boolean load = ( (RolapCube) getCube() ).isLoadInProgress();
    return !strictValidation && ( load ? props.IgnoreInvalidMembers.get() : props.IgnoreInvalidMembersDuringQuery
        .get() );
  }

  /**
   * A Query's ResultStyle can only be one of the following: ResultStyle.ITERABLE ResultStyle.LIST
   * ResultStyle.MUTABLE_LIST
   */
  public void setResultStyle( ResultStyle resultStyle ) {
    switch ( resultStyle ) {
      case ITERABLE:
        // For java4, use LIST
        this.resultStyle = ( Util.Retrowoven ) ? ResultStyle.LIST : ResultStyle.ITERABLE;
        break;
      case LIST:
      case MUTABLE_LIST:
        this.resultStyle = resultStyle;
        break;
      default:
        throw ResultStyleException.generateBadType( ResultStyle.ITERABLE_LIST_MUTABLELIST, resultStyle );
    }
  }

  public ResultStyle getResultStyle() {
    return resultStyle;
  }

  /**
   * Generates compiled forms of all expressions.
   *
   * @param compiler
   *          Compiler
   */
  private void compile( ExpCompiler compiler ) {
    if ( formulas != null ) {
      for ( Formula formula : formulas ) {
        formula.compile();
      }
    }

    if ( axes != null ) {
      axisCalcs = new Calc[axes.length];
      for ( int i = 0; i < axes.length; i++ ) {
        axisCalcs[i] = axes[i].compile( compiler, resultStyle );
      }
    }
    if ( slicerAxis != null ) {
      slicerCalc = slicerAxis.compile( compiler, resultStyle );
    }
  }

  /**
   * Performs type-checking and validates internal consistency of a query.
   *
   * @param validator
   *          Validator
   */
  public void resolve( Validator validator ) {
    // Register all parameters.
    parameters.clear();
    parametersByName.clear();
    accept( new ParameterFinder() );

    // Register all aliased expressions ('expr AS alias') as named sets.
    accept( new AliasedExpressionFinder() );

    // Validate formulas.
    if ( formulas != null ) {
      for ( Formula formula : formulas ) {
        validator.validate( formula );
      }
    }

    // Validate axes.
    if ( axes != null ) {
      Set<Integer> axisNames = new HashSet<Integer>();
      for ( QueryAxis axis : axes ) {
        validator.validate( axis );
        if ( !axisNames.add( axis.getAxisOrdinal().logicalOrdinal() ) ) {
          throw MondrianResource.instance().DuplicateAxis.ex( axis.getAxisName() );
        }
      }

      // Make sure that there are no gaps. If there are N axes, then axes
      // 0 .. N-1 should exist.
      int seekOrdinal = AxisOrdinal.StandardAxisOrdinal.COLUMNS.logicalOrdinal();
      for ( QueryAxis axis : axes ) {
        if ( !axisNames.contains( seekOrdinal ) ) {
          AxisOrdinal axisName = AxisOrdinal.StandardAxisOrdinal.forLogicalOrdinal( seekOrdinal );
          throw MondrianResource.instance().NonContiguousAxis.ex( seekOrdinal, axisName.name() );
        }
        ++seekOrdinal;
      }
    }
    if ( slicerAxis != null ) {
      slicerAxis.validate( validator );
    }

    // Make sure that no hierarchy is used on more than one axis.
    for ( Hierarchy hierarchy : ( (RolapCube) getCube() ).getHierarchies() ) {
      int useCount = 0;
      for ( QueryAxis axis : allAxes() ) {
        if ( axis.getSet().getType().usesHierarchy( hierarchy, true ) ) {
          ++useCount;
        }
      }
      if ( useCount > 1 ) {
        throw MondrianResource.instance().HierarchyInIndependentAxes.ex( hierarchy.getUniqueName() );
      }
    }
  }

  @Override
  public void explain( PrintWriter pw ) {
    final boolean profiling = getStatement().getProfileHandler() != null;
    final CalcWriter calcWriter = new CalcWriter( pw, profiling );
    for ( Formula formula : formulas ) {
      formula.getMdxMember(); // TODO:
    }
    if ( slicerCalc != null ) {
      pw.println( "Axis (FILTER):" );
      slicerCalc.accept( calcWriter );
      pw.println();
    }
    int i = -1;
    for ( QueryAxis axis : axes ) {
      ++i;
      pw.println( "Axis (" + axis.getAxisName() + "):" );
      axisCalcs[i].accept( calcWriter );
      pw.println();
    }
    pw.flush();
  }

  /**
   * Returns a collection of all axes, including the slicer as the first element, if there is a slicer.
   *
   * @return Collection of all axes including slicer
   */
  private Collection<QueryAxis> allAxes() {
    if ( slicerAxis == null ) {
      return Arrays.asList( axes );
    } else {
      // noinspection unchecked
      return new CompositeCollection( new Collection[] { Collections.singletonList( slicerAxis ), Arrays.asList(
          axes ) } );
    }
  }

  public void unparse( PrintWriter pw ) {
    if ( formulas != null ) {
      for ( int i = 0; i < formulas.length; i++ ) {
        if ( i == 0 ) {
          pw.print( "with " );
        } else {
          pw.print( "  " );
        }
        formulas[i].unparse( pw );
        pw.println();
      }
    }
    pw.print( "select " );
    if ( axes != null ) {
      for ( int i = 0; i < axes.length; i++ ) {
        axes[i].unparse( pw );
        if ( i < axes.length - 1 ) {
          pw.println( "," );
          pw.print( "  " );
        } else {
          pw.println();
        }
      }
    }
    if ( cube != null ) {
      pw.println( "from [" + cube.getName() + "]" );
    }
    if ( slicerAxis != null ) {
      pw.print( "where " );
      slicerAxis.unparse( pw );
      pw.println();
    }
  }

  /** Returns the MDX query string. */
  public String toString() {
    resolve();
    return Util.unparse( this );
  }

  public Object[] getChildren() {
    // Chidren are axes, slicer, and formulas (in that order, to be
    // consistent with replaceChild).
    List<QueryPart> list = new ArrayList<QueryPart>();
    list.addAll( Arrays.asList( axes ) );
    if ( slicerAxis != null ) {
      list.add( slicerAxis );
    }
    list.addAll( Arrays.asList( formulas ) );
    return list.toArray();
  }

  public QueryAxis getSlicerAxis() {
    return slicerAxis;
  }

  public void setSlicerAxis( QueryAxis axis ) {
    this.slicerAxis = axis;
  }

  /**
   * Adds a level to an axis expression.
   */
  public void addLevelToAxis( AxisOrdinal axis, Level level ) {
    assert axis != null;
    axes[axis.logicalOrdinal()].addLevel( level );
  }

  /**
   * Returns the hierarchies in an expression.
   *
   * <p>
   * If the expression's type is a dimension with several hierarchies, assumes that the expression yields a member of
   * the first (default) hierarchy of the dimension.
   *
   * <p>
   * For example, the expression <blockquote><code>Crossjoin(
   *   Hierarchize(
   *     Union(
   *       {[Time].LastSibling}, [Time].LastSibling.Children)),
   *       {[Measures].[Unit Sales], [Measures].[Store Cost]})</code> </blockquote>
   *
   * has type <code>{[Time.Monthly], [Measures]}</code> even though <code>[Time].LastSibling</code> might return a
   * member of either [Time.Monthly] or [Time.Weekly].
   */
  private Hierarchy[] collectHierarchies( Exp queryPart ) {
    Type exprType = queryPart.getType();
    if ( exprType instanceof SetType ) {
      exprType = ( (SetType) exprType ).getElementType();
    }
    if ( exprType instanceof TupleType ) {
      final Type[] types = ( (TupleType) exprType ).elementTypes;
      ArrayList<Hierarchy> hierarchyList = new ArrayList<Hierarchy>();
      for ( Type type : types ) {
        hierarchyList.add( getTypeHierarchy( type ) );
      }
      return hierarchyList.toArray( new Hierarchy[hierarchyList.size()] );
    }
    return new Hierarchy[] { getTypeHierarchy( exprType ) };
  }

  private Hierarchy getTypeHierarchy( final Type type ) {
    Hierarchy hierarchy = type.getHierarchy();
    if ( hierarchy != null ) {
      return hierarchy;
    }
    final Dimension dimension = type.getDimension();
    if ( dimension != null ) {
      return dimension.getHierarchy();
    }
    return null;
  }

  /**
   * Assigns a value to the parameter with a given name.
   *
   * @throws RuntimeException
   *           if there is not parameter with the given name
   */
  public void setParameter( final String parameterName, final Object value ) {
    // Need to resolve query before we set parameters, in order to create
    // slots to store them in. (This code will go away when parameters
    // belong to prepared statements.)
    if ( parameters.isEmpty() ) {
      resolve();
    }

    final Parameter param = getSchemaReader( false ).getParameter( parameterName );
    if ( param == null ) {
      throw MondrianResource.instance().UnknownParameter.ex( parameterName );
    }
    if ( !param.isModifiable() ) {
      throw MondrianResource.instance().ParameterIsNotModifiable.ex( parameterName, param.getScope().name() );
    }
    final Object value2 =
        Locus.execute( new Execution( statement, 0 ), "Query.quickParse", new Locus.Action<Object>() {
          public Object execute() {
            return quickParse( parameterName, param.getType(), value, Query.this );
          }
        } );
    param.setValue( value2 );
  }

  /**
   * Converts a value into something appropriate for a given type.
   *
   * <p>
   * Viz:
   * <ul>
   * <li>For numerics, takes number or string and returns a {@link Number}.
   * <li>For strings, takes string, or calls {@link Object#toString()} on any other type
   * <li>For members, takes member or string
   * <li>For sets of members, requires a list of members or strings and converts each element to a member.
   * </ul>
   *
   * @param type
   *          Type
   * @param value
   *          Value
   * @param query
   *          Query
   * @return Value of appropriate type
   * @throws NumberFormatException
   *           If value needs to be a number but isn't
   */
  private static Object quickParse( String parameterName, Type type, Object value, Query query ) {
    int category = TypeUtil.typeToCategory( type );
    switch ( category ) {
      case Category.Numeric:
        if ( value instanceof Number || value == null ) {
          return value;
        }
        if ( value instanceof String ) {
          String s = (String) value;
          try {
            return new Integer( s );
          } catch ( NumberFormatException e ) {
            return new Double( s );
          }
        }
        throw Util.newInternal( "Invalid value '" + value + "' for parameter '" + parameterName + "', type " + type );
      case Category.String:
        if ( value == null ) {
          return null;
        }
        return value.toString();
      case Category.Set:
        if ( value instanceof String ) {
          value = IdentifierParser.parseIdentifierList( (String) value );
        }
        if ( !( value instanceof List ) ) {
          throw Util.newInternal( "Invalid value '" + value + "' for parameter '" + parameterName + "', type "
              + type );
        }
        List<Member> expList = new ArrayList<Member>();
        final List list = (List) value;
        final SetType setType = (SetType) type;
        final Type elementType = setType.getElementType();
        for ( Object o : list ) {
          // In keeping with MDX semantics, null members are omitted from
          // lists.
          if ( o == null ) {
            continue;
          }
          final Member member = (Member) quickParse( parameterName, elementType, o, query );
          expList.add( member );
        }
        return expList;
      case Category.Member:
        if ( value == null ) {
          // Setting a member parameter to null is the same as setting to
          // the null member of the hierarchy. May not be equivalent to
          // the default value of the parameter, nor the same as the all
          // member.
          if ( type.getHierarchy() != null ) {
            value = type.getHierarchy().getNullMember();
          } else if ( type.getDimension() != null ) {
            value = type.getDimension().getHierarchy().getNullMember();
          }
        }
        if ( value instanceof String ) {
          value = Util.parseIdentifier( (String) value );
        }
        if ( value instanceof List && Util.canCast( (List) value, Id.Segment.class ) ) {
          final List<Id.Segment> segmentList = Util.cast( (List) value );
          final OlapElement olapElement = Util.lookup( query, segmentList );
          if ( olapElement instanceof Member ) {
            value = olapElement;
          }
        }
        if ( value instanceof List && Util.canCast( (List) value, IdentifierSegment.class ) ) {
          final List<IdentifierSegment> olap4jSegmentList = Util.cast( (List) value );
          final List<Id.Segment> segmentList = Util.convert( olap4jSegmentList );
          final OlapElement olapElement = Util.lookup( query, segmentList );
          if ( olapElement instanceof Member ) {
            value = olapElement;
          }
        }
        if ( value instanceof Member ) {
          if ( type.isInstance( value ) ) {
            return value;
          }
        }
        throw Util.newInternal( "Invalid value '" + value + "' for parameter '" + parameterName + "', type " + type );
      default:
        throw Category.instance.badValue( category );
    }
  }

  /**
   * Swaps the x- and y- axes. Does nothing if the number of axes != 2.
   */
  public void swapAxes() {
    if ( axes.length == 2 ) {
      Exp e0 = axes[0].getSet();
      boolean nonEmpty0 = axes[0].isNonEmpty();
      Exp e1 = axes[1].getSet();
      boolean nonEmpty1 = axes[1].isNonEmpty();
      axes[1].setSet( e0 );
      axes[1].setNonEmpty( nonEmpty0 );
      axes[0].setSet( e1 );
      axes[0].setNonEmpty( nonEmpty1 );
      // showSubtotals ???
    }
  }

  /**
   * Returns the parameters defined in this query.
   */
  public Parameter[] getParameters() {
    return parameters.toArray( new Parameter[parameters.size()] );
  }

  public Cube getCube() {
    return cube;
  }

  /**
   * Returns a schema reader.
   *
   * @param accessControlled
   *          If true, schema reader returns only elements which are accessible to the statement's current role
   *
   * @return schema reader
   */
  public SchemaReader getSchemaReader( boolean accessControlled ) {
    final Role role;
    if ( accessControlled ) {
      // full access control
      role = getConnection().getRole();
    } else {
      role = null;
    }
    final SchemaReader cubeSchemaReader = cube.getSchemaReader( role );
    return new QuerySchemaReader( cubeSchemaReader, Query.this );
  }

  /**
   * Looks up a member whose unique name is <code>memberUniqueName</code> from cache. If the member is not in cache,
   * returns null.
   */
  public Member lookupMemberFromCache( String memberUniqueName ) {
    // first look in defined members
    for ( Member member : getDefinedMembers() ) {
      if ( Util.equalName( member.getUniqueName(), memberUniqueName ) || Util.equalName( getUniqueNameWithoutAll(
          member ), memberUniqueName ) ) {
        return member;
      }
    }
    return null;
  }

  private String getUniqueNameWithoutAll( Member member ) {
    // build unique string
    Member parentMember = member.getParentMember();
    if ( ( parentMember != null ) && !parentMember.isAll() ) {
      return Util.makeFqName( getUniqueNameWithoutAll( parentMember ), member.getName() );
    } else {
      return Util.makeFqName( member.getHierarchy(), member.getName() );
    }
  }

  /**
   * Looks up a named set.
   */
  private NamedSet lookupNamedSet( Id.Segment segment ) {
    if ( !( segment instanceof Id.NameSegment ) ) {
      return null;
    }
    Id.NameSegment nameSegment = (Id.NameSegment) segment;
    for ( Formula formula : formulas ) {
      if ( !formula.isMember() && formula.getElement() != null && formula.getName().equals( nameSegment.getName() ) ) {
        return (NamedSet) formula.getElement();
      }
    }
    return null;
  }

  /**
   * Creates a named set defined by an alias.
   */
  public ScopedNamedSet createScopedNamedSet( String name, QueryPart scope, Exp expr ) {
    final ScopedNamedSet scopedNamedSet = new ScopedNamedSet( name, scope, expr );
    scopedNamedSets.add( scopedNamedSet );
    return scopedNamedSet;
  }

  /**
   * Looks up a named set defined by an alias.
   *
   * @param nameParts
   *          Multi-part identifier for set
   * @param scopeList
   *          Parse tree node where name is used (last in list) and
   */
  ScopedNamedSet lookupScopedNamedSet( List<Id.Segment> nameParts, ArrayStack<QueryPart> scopeList ) {
    if ( nameParts.size() != 1 ) {
      return null;
    }
    if ( !( nameParts.get( 0 ) instanceof Id.NameSegment ) ) {
      return null;
    }
    String name = ( (Id.NameSegment) nameParts.get( 0 ) ).getName();
    ScopedNamedSet bestScopedNamedSet = null;
    int bestScopeOrdinal = -1;
    for ( ScopedNamedSet scopedNamedSet : scopedNamedSets ) {
      if ( Util.equalName( scopedNamedSet.name, name ) ) {
        int scopeOrdinal = scopeList.indexOf( scopedNamedSet.scope );
        if ( scopeOrdinal > bestScopeOrdinal ) {
          bestScopedNamedSet = scopedNamedSet;
          bestScopeOrdinal = scopeOrdinal;
        }
      }
    }
    return bestScopedNamedSet;
  }

  /**
   * Returns an array of the formulas used in this query.
   */
  public Formula[] getFormulas() {
    return formulas;
  }

  /**
   * Returns an array of this query's axes.
   */
  public QueryAxis[] getAxes() {
    return axes;
  }

  /**
   * Remove a formula from the query. If <code>failIfUsedInQuery</code> is true, checks and throws an error if formula
   * is used somewhere in the query.
   */
  public void removeFormula( String uniqueName, boolean failIfUsedInQuery ) {
    Formula formula = findFormula( uniqueName );
    if ( failIfUsedInQuery && formula != null ) {
      OlapElement mdxElement = formula.getElement();
      // search the query tree to see if this formula expression is used
      // anywhere (on the axes or in another formula)
      Walker walker = new Walker( this );
      while ( walker.hasMoreElements() ) {
        Object queryElement = walker.nextElement();
        if ( !queryElement.equals( mdxElement ) ) {
          continue;
        }
        // mdxElement is used in the query. lets find on on which axis
        // or formula
        String formulaType =
            formula.isMember() ? MondrianResource.instance().CalculatedMember.str() : MondrianResource
                .instance().CalculatedSet.str();

        int i = 0;
        Object parent = walker.getAncestor( i );
        Object grandParent = walker.getAncestor( i + 1 );
        while ( ( parent != null ) && ( grandParent != null ) ) {
          if ( grandParent instanceof Query ) {
            if ( parent instanceof Axis ) {
              throw MondrianResource.instance().MdxCalculatedFormulaUsedOnAxis.ex( formulaType, uniqueName,
                  ( (QueryAxis) parent ).getAxisName() );

            } else if ( parent instanceof Formula ) {
              String parentFormulaType =
                  ( (Formula) parent ).isMember() ? MondrianResource.instance().CalculatedMember.str()
                      : MondrianResource.instance().CalculatedSet.str();
              throw MondrianResource.instance().MdxCalculatedFormulaUsedInFormula.ex( formulaType, uniqueName,
                  parentFormulaType, ( (Formula) parent ).getUniqueName() );

            } else {
              throw MondrianResource.instance().MdxCalculatedFormulaUsedOnSlicer.ex( formulaType, uniqueName );
            }
          }
          ++i;
          parent = walker.getAncestor( i );
          grandParent = walker.getAncestor( i + 1 );
        }
        throw MondrianResource.instance().MdxCalculatedFormulaUsedInQuery.ex( formulaType, uniqueName, Util.unparse(
            this ) );
      }
    }

    // remove formula from query
    List<Formula> formulaList = new ArrayList<Formula>();
    for ( Formula formula1 : formulas ) {
      if ( !formula1.getUniqueName().equalsIgnoreCase( uniqueName ) ) {
        formulaList.add( formula1 );
      }
    }

    // it has been found and removed
    this.formulas = formulaList.toArray( new Formula[formulaList.size()] );
  }

  /**
   * Returns whether a formula can safely be removed from the query. It can be removed if the member or set it defines
   * it not used anywhere else in the query, including in another formula.
   *
   * @param uniqueName
   *          Unique name of the member or set defined by the formula
   * @return whether the formula can safely be removed
   */
  public boolean canRemoveFormula( String uniqueName ) {
    Formula formula = findFormula( uniqueName );
    if ( formula == null ) {
      return false;
    }

    OlapElement mdxElement = formula.getElement();
    // Search the query tree to see if this formula expression is used
    // anywhere (on the axes or in another formula).
    Walker walker = new Walker( this );
    while ( walker.hasMoreElements() ) {
      Object queryElement = walker.nextElement();
      if ( queryElement instanceof MemberExpr && ( (MemberExpr) queryElement ).getMember().equals( mdxElement ) ) {
        return false;
      }
      if ( queryElement instanceof NamedSetExpr && ( (NamedSetExpr) queryElement ).getNamedSet().equals(
          mdxElement ) ) {
        return false;
      }
    }
    return true;
  }

  /**
   * Looks up a calculated member or set defined in this Query.
   *
   * @param uniqueName
   *          Unique name of calculated member or set
   * @return formula defining calculated member, or null if not found
   */
  public Formula findFormula( String uniqueName ) {
    for ( Formula formula : formulas ) {
      if ( formula.getUniqueName().equalsIgnoreCase( uniqueName ) ) {
        return formula;
      }
    }
    return null;
  }

  /**
   * Finds formula by name and renames it to new name.
   */
  public void renameFormula( String uniqueName, String newName ) {
    Formula formula = findFormula( uniqueName );
    if ( formula == null ) {
      throw MondrianResource.instance().MdxFormulaNotFound.ex( "formula", uniqueName, Util.unparse( this ) );
    }
    formula.rename( newName );
  }

  List<Member> getDefinedMembers() {
    List<Member> definedMembers = new ArrayList<Member>();
    for ( final Formula formula : formulas ) {
      if ( formula.isMember() && formula.getElement() != null && getConnection().getRole().canAccess( formula
          .getElement() ) ) {
        definedMembers.add( (Member) formula.getElement() );
      }
    }
    return definedMembers;
  }

  /**
   * Finds axis by index and sets flag to show empty cells on that axis.
   */
  public void setAxisShowEmptyCells( int axis, boolean showEmpty ) {
    if ( axis >= axes.length ) {
      throw MondrianResource.instance().MdxAxisShowSubtotalsNotSupported.ex( axis );
    }
    axes[axis].setNonEmpty( !showEmpty );
  }

  /**
   * Returns <code>Hierarchy[]</code> used on <code>axis</code>. It calls {@link #collectHierarchies}.
   */
  public Hierarchy[] getMdxHierarchiesOnAxis( AxisOrdinal axis ) {
    if ( axis.logicalOrdinal() >= axes.length ) {
      throw MondrianResource.instance().MdxAxisShowSubtotalsNotSupported.ex( axis.logicalOrdinal() );
    }
    QueryAxis queryAxis = axis.isFilter() ? slicerAxis : axes[axis.logicalOrdinal()];
    return collectHierarchies( queryAxis.getSet() );
  }

  /**
   * Compiles an expression, using a cached compiled expression if available.
   *
   * @param exp
   *          Expression
   * @param scalar
   *          Whether expression is scalar
   * @param resultStyle
   *          Preferred result style; if null, use query's default result style; ignored if expression is scalar
   * @return compiled expression
   */
  public Calc compileExpression( Exp exp, boolean scalar, ResultStyle resultStyle ) {
    // REVIEW: Set query on a connection's shared internal statement is
    // not re-entrant.
    statement.setQuery( this );
    Evaluator evaluator = RolapEvaluator.create( statement );
    final Validator validator = createValidator();
    List<ResultStyle> resultStyleList;
    resultStyleList = Collections.singletonList( resultStyle != null ? resultStyle : this.resultStyle );
    final ExpCompiler compiler = createCompiler( evaluator, validator, resultStyleList );
    if ( scalar ) {
      return compiler.compileScalar( exp, false );
    } else {
      return compiler.compile( exp );
    }
  }

  public ExpCompiler createCompiler() {
    // REVIEW: Set query on a connection's shared internal statement is
    // not re-entrant.
    statement.setQuery( this );
    Evaluator evaluator = RolapEvaluator.create( statement );
    Validator validator = createValidator();
    return createCompiler( evaluator, validator, Collections.singletonList( resultStyle ) );
  }

  private ExpCompiler createCompiler( final Evaluator evaluator, final Validator validator,
      List<ResultStyle> resultStyleList ) {
    ExpCompiler compiler = ExpCompiler.Factory.getExpCompiler( evaluator, validator, resultStyleList );

    final int expDeps = MondrianProperties.instance().TestExpDependencies.get();
    final ProfileHandler profileHandler = statement.getProfileHandler();
    if ( profileHandler != null ) {
      // Cannot test dependencies and profile at the same time. Profiling
      // trumps.
      compiler = RolapUtil.createProfilingCompiler( compiler );
    } else if ( expDeps > 0 ) {
      compiler = RolapUtil.createDependencyTestingCompiler( compiler );
    }
    return compiler;
  }

  /**
   * Keeps track of references to members of the measures dimension
   *
   * @param olapElement
   *          potential measure member
   */
  public void addMeasuresMembers( OlapElement olapElement ) {
    if ( olapElement instanceof Member ) {
      Member member = (Member) olapElement;
      if ( member.isMeasure() ) {
        measuresMembers.add( member );
      }
    }
  }

  /**
   * @return set of members from the measures dimension referenced within this query
   */
  public Set<Member> getMeasuresMembers() {
    return Collections.unmodifiableSet( measuresMembers );
  }

  /**
   * Indicates that the query cannot use native cross joins to process this virtual cube
   */
  public void setVirtualCubeNonNativeCrossJoin() {
    nativeCrossJoinVirtualCube = false;
  }

  /**
   * @return true if the query can use native cross joins on a virtual cube
   */
  public boolean nativeCrossJoinVirtualCube() {
    return nativeCrossJoinVirtualCube;
  }

  /**
   * Saves away the base cubes related to the virtual cube referenced in this query
   *
   * @param baseCubes
   *          set of base cubes
   */
  public void setBaseCubes( List<RolapCube> baseCubes ) {
    this.baseCubes = baseCubes;
  }

  /**
   * return the set of base cubes associated with the virtual cube referenced in this query
   *
   * @return set of base cubes
   */
  public List<RolapCube> getBaseCubes() {
    return baseCubes;
  }

  public Object accept( MdxVisitor visitor ) {
    Object o = visitor.visit( this );

    if ( visitor.shouldVisitChildren() ) {
      // visit formulas
      for ( Formula formula : formulas ) {
        formula.accept( visitor );
      }
      // visit axes
      for ( QueryAxis axis : axes ) {
        axis.accept( visitor );
      }
      if ( slicerAxis != null ) {
        slicerAxis.accept( visitor );
      }
    }
    return o;
  }

  /**
   * Put an Object value into the evaluation cache with given key. This is used by Calc's to store information between
   * iterations (rather than re-generate each time).
   *
   * @param key
   *          the cache key
   * @param value
   *          the cache value
   */
  public void putEvalCache( String key, Object value ) {
    evalCache.put( key, value );
  }

  /**
   * Gets the Object associated with the value.
   *
   * @param key
   *          the cache key
   * @return the cached value or null.
   */
  public Object getEvalCache( String key ) {
    return evalCache.get( key );
  }

  /**
   * Remove all entries in the evaluation cache
   */
  public void clearEvalCache() {
    evalCache.clear();
  }

  /**
   * Closes this query.
   *
   * <p>
   * Releases any resources held. Writes statistics to log if profiling is enabled.
   *
   * <p>
   * This method is idempotent.
   *
   * @deprecated This method will be removed in mondrian-4.0.
   */
  public void close() {
    if ( ownStatement ) {
      statement.close();
    }
  }

  public Statement getStatement() {
    return statement;
  }

  /**
   * Sets that the query owns its statement; therefore it will need to close it when the query is closed.
   *
   * @param ownStatement
   *          Whether the statement belongs to the query
   */
  public void setOwnStatement( boolean ownStatement ) {
    this.ownStatement = ownStatement;
  }

  /**
   * Source of metadata within the scope of a query.
   *
   * <p>
   * Note especially that {@link #getCalculatedMember(java.util.List)} returns the calculated members defined in this
   * query. It does not perform access control; all calculated members defined in a query are visible to everyone.
   */
  private static class QuerySchemaReader extends DelegatingSchemaReader implements NameResolver.Namespace {
    private final Query query;

    public QuerySchemaReader( SchemaReader cubeSchemaReader, Query query ) {
      super( cubeSchemaReader );
      this.query = query;
    }

    public SchemaReader withoutAccessControl() {
      return new QuerySchemaReader( schemaReader.withoutAccessControl(), query );
    }

    @Override
    public Member getMemberByUniqueName( List<Id.Segment> uniqueNameParts, boolean failIfNotFound,
        MatchType matchType ) {
      final String uniqueName = Util.implode( uniqueNameParts );
      Member member = query.lookupMemberFromCache( uniqueName );
      if ( member == null ) {
        // Not a calculated member in the query, so go to the cube.
        member = schemaReader.getMemberByUniqueName( uniqueNameParts, failIfNotFound, matchType );
      }
      if ( !failIfNotFound && member == null ) {
        return null;
      }
      if ( getRole().canAccess( member ) ) {
        return member;
      } else {
        return null;
      }
    }

    @Override
    public List<Member> getLevelMembers( Level level, boolean includeCalculated ) {
      List<Member> members = super.getLevelMembers( level, false );
      if ( includeCalculated ) {
        members = Util.addLevelCalculatedMembers( this, level, members );
      }
      return members;
    }

    public Member getCalculatedMember( List<Id.Segment> nameParts ) {
      for ( final Formula formula : query.formulas ) {
        if ( !formula.isMember() ) {
          continue;
        }
        Member member = (Member) formula.getElement();
        if ( member == null ) {
          continue;
        }
        if ( !Util.matches( member, nameParts ) ) {
          continue;
        }
        if ( !query.getConnection().getRole().canAccess( member ) ) {
          continue;
        }
        return member;
      }
      return null;
    }

    public List<Member> getCalculatedMembers( Hierarchy hierarchy ) {
      List<Member> result = new ArrayList<Member>();
      // Add calculated members in the cube.
      final List<Member> calculatedMembers = super.getCalculatedMembers( hierarchy );
      result.addAll( calculatedMembers );
      // Add calculated members defined in the query.
      for ( Member member : query.getDefinedMembers() ) {
        if ( member.getHierarchy().equals( hierarchy ) ) {
          result.add( member );
        }
      }
      return result;
    }

    public List<Member> getCalculatedMembers( Level level ) {
      List<Member> hierarchyMembers = getCalculatedMembers( level.getHierarchy() );
      List<Member> result = new ArrayList<Member>();
      for ( Member member : hierarchyMembers ) {
        if ( member.getLevel().equals( level ) ) {
          result.add( member );
        }
      }
      return result;
    }

    public List<Member> getCalculatedMembers() {
      return query.getDefinedMembers();
    }

    @Override
    public OlapElement getElementChild( OlapElement parent, Id.Segment s ) {
      return getElementChild( parent, s, MatchType.EXACT );
    }

    public OlapElement getElementChild( OlapElement parent, Id.Segment s, MatchType matchType ) {
      // first look in cube
      OlapElement mdxElement = schemaReader.getElementChild( parent, s, matchType );
      if ( mdxElement != null ) {
        return mdxElement;
      }
      // then look in defined members (fixes MONDRIAN-77)

      // then in defined sets
      if ( !( s instanceof Id.NameSegment ) ) {
        return null;
      }
      String name = ( (Id.NameSegment) s ).getName();
      for ( Formula formula : query.formulas ) {
        if ( formula.isMember() ) {
          continue; // have already done these
        }
        Id id = formula.getIdentifier();
        if ( id.getSegments().size() == 1 && id.getSegments().get( 0 ).matches( name ) ) {
          return formula.getNamedSet();
        }
      }

      return mdxElement;
    }

    @Override
    public OlapElement lookupCompoundInternal( OlapElement parent, List<Id.Segment> names, boolean failIfNotFound,
        int category, MatchType matchType ) {
      if ( matchType == MatchType.EXACT ) {
        OlapElement oe = lookupCompound( parent, names, failIfNotFound, category, MatchType.EXACT_SCHEMA );
        if ( oe != null ) {
          return oe;
        }
      }
      // First look to ourselves.
      switch ( category ) {
        case Category.Unknown:
        case Category.Member:
          if ( parent == query.cube ) {
            final Member calculatedMember = getCalculatedMember( names );
            if ( calculatedMember != null ) {
              return calculatedMember;
            }
          }
      }
      switch ( category ) {
        case Category.Unknown:
        case Category.Set:
          if ( parent == query.cube ) {
            final NamedSet namedSet = getNamedSet( names );
            if ( namedSet != null ) {
              return namedSet;
            }
          }
      }
      // Then delegate to the next reader.
      OlapElement olapElement = super.lookupCompoundInternal( parent, names, failIfNotFound, category, matchType );
      if ( olapElement instanceof Member ) {
        Member member = (Member) olapElement;
        final Formula formula = (Formula) member.getPropertyValue( Property.FORMULA.name );
        if ( formula != null ) {
          // This is a calculated member defined against the cube.
          // Create a free-standing formula using the same
          // expression, then use the member defined in that formula.
          final Formula formulaClone = (Formula) formula.clone();
          formulaClone.createElement( query );
          formulaClone.accept( query.createValidator() );
          olapElement = formulaClone.getMdxMember();
        }
      }
      return olapElement;
    }

    public NamedSet getNamedSet( List<Id.Segment> nameParts ) {
      if ( nameParts.size() != 1 ) {
        return null;
      }
      return query.lookupNamedSet( nameParts.get( 0 ) );
    }

    public Parameter getParameter( String name ) {
      // Look for a parameter defined in the query.
      for ( Parameter parameter : query.parameters ) {
        if ( parameter.getName().equals( name ) ) {
          return parameter;
        }
      }

      // Look for a parameter defined in this statement.
      if ( Util.lookup( RolapConnectionProperties.class, name ) != null ) {
        Object value = query.statement.getProperty( name );
        // TODO: Don't assume it's a string.
        // TODO: Create expression which will get the value from the
        // statement at the time the query is executed.
        Literal defaultValue = Literal.createString( String.valueOf( value ) );
        return new ConnectionParameterImpl( name, defaultValue );
      }

      return super.getParameter( name );
    }

    public OlapElement lookupChild( OlapElement parent, IdentifierSegment segment, MatchType matchType ) {
      // ignore matchType
      return lookupChild( parent, segment );
    }

    public OlapElement lookupChild( OlapElement parent, IdentifierSegment segment ) {
      // Only look for calculated members and named sets defined in the
      // query.
      for ( Formula formula : query.getFormulas() ) {
        if ( NameResolver.matches( formula, parent, segment ) ) {
          return formula.getElement();
        }
      }
      return null;
    }

    public List<NameResolver.Namespace> getNamespaces() {
      final List<NameResolver.Namespace> list = new ArrayList<>();
      list.add( this );
      list.addAll( super.getNamespaces() );
      return list;
    }
  }

  private static class ConnectionParameterImpl extends ParameterImpl {
    public ConnectionParameterImpl( String name, Literal defaultValue ) {
      super( name, defaultValue, "Connection property", new StringType() );
    }

    public Scope getScope() {
      return Scope.Connection;
    }

    public void setValue( Object value ) {
      throw MondrianResource.instance().ParameterIsNotModifiable.ex( getName(), getScope().name() );
    }
  }

  /**
   * Implementation of {@link mondrian.olap.Validator} that works within a particular query.
   *
   * <p>
   * It's unlikely that we would want a validator that is NOT within a particular query, but by organizing the code this
   * way, with the majority of the code in {@link mondrian.olap.ValidatorImpl}, the dependencies between Validator and
   * Query are explicit.
   */
  private static class QueryValidator extends ValidatorImpl {
    private final boolean alwaysResolveFunDef;
    private Query query;
    private final SchemaReader schemaReader;

    /**
     * Creates a QueryValidator.
     *
     * @param functionTable
     *          Function table
     * @param alwaysResolveFunDef
     *          Whether to always resolve function definitions (see {@link #alwaysResolveFunDef()})
     * @param query
     *          Query
     */
    public QueryValidator( FunTable functionTable, boolean alwaysResolveFunDef, Query query,
        Map<QueryPart, QueryPart> resolvedIdentifiers ) {
      super( functionTable, resolvedIdentifiers );
      this.alwaysResolveFunDef = alwaysResolveFunDef;
      this.query = query;
      this.schemaReader = new ScopedSchemaReader( this, true );
    }

    public SchemaReader getSchemaReader() {
      return schemaReader;
    }

    protected void defineParameter( Parameter param ) {
      final String name = param.getName();
      query.parameters.add( param );
      query.parametersByName.put( name, param );
    }

    public Query getQuery() {
      return query;
    }

    public boolean alwaysResolveFunDef() {
      return alwaysResolveFunDef;
    }

    public ArrayStack<QueryPart> getScopeStack() {
      return stack;
    }
  }

  /**
   * Schema reader that depends on the current scope during the validation of a query. Depending on the scope, different
   * calculated sets may be visible. The scope is represented by the expression stack inside the validator.
   */
  private static class ScopedSchemaReader extends DelegatingSchemaReader implements NameResolver.Namespace {
    private final QueryValidator queryValidator;
    private final boolean accessControlled;

    /**
     * Creates a ScopedSchemaReader.
     *
     * @param queryValidator
     *          Validator that is being used to validate the query
     * @param accessControlled
     *          Access controlled
     */
    private ScopedSchemaReader( QueryValidator queryValidator, boolean accessControlled ) {
      super( queryValidator.getQuery().getSchemaReader( accessControlled ) );
      this.queryValidator = queryValidator;
      this.accessControlled = accessControlled;
    }

    public SchemaReader withoutAccessControl() {
      if ( !accessControlled ) {
        return this;
      }
      return new ScopedSchemaReader( queryValidator, false );
    }

    public List<NameResolver.Namespace> getNamespaces() {
      final List<NameResolver.Namespace> list = new ArrayList<>();
      list.add( this );
      list.addAll( super.getNamespaces() );
      return list;
    }

    @Override
    public OlapElement lookupCompoundInternal( OlapElement parent, final List<Id.Segment> names,
        boolean failIfNotFound, int category, MatchType matchType ) {
      switch ( category ) {
        case Category.Set:
        case Category.Unknown:
          final ScopedNamedSet namedSet =
              queryValidator.getQuery().lookupScopedNamedSet( names, queryValidator.getScopeStack() );
          if ( namedSet != null ) {
            return namedSet;
          }
      }
      return super.lookupCompoundInternal( parent, names, failIfNotFound, category, matchType );
    }

    public OlapElement lookupChild( OlapElement parent, IdentifierSegment segment, MatchType matchType ) {
      // ignore matchType
      return lookupChild( parent, segment );
    }

    public OlapElement lookupChild( OlapElement parent, IdentifierSegment segment ) {
      if ( !( parent instanceof Cube ) ) {
        return null;
      }
      return queryValidator.getQuery().lookupScopedNamedSet( Collections.singletonList( Util.convert( segment ) ),
          queryValidator.getScopeStack() );
    }
  }

  public static class ScopedNamedSet implements NamedSet {
    private final String name;
    private final QueryPart scope;
    private Exp expr;

    /**
     * Creates a ScopedNamedSet.
     *
     * @param name
     *          Name
     * @param scope
     *          Scope of named set (the function call that encloses the 'expr AS name', often GENERATE or FILTER)
     * @param expr
     *          Expression that defines the set
     */
    private ScopedNamedSet( String name, QueryPart scope, Exp expr ) {
      this.name = name;
      this.scope = scope;
      this.expr = expr;
    }

    public String getName() {
      return name;
    }

    public String getNameUniqueWithinQuery() {
      return System.identityHashCode( this ) + "";
    }

    public boolean isDynamic() {
      return true;
    }

    public Exp getExp() {
      return expr;
    }

    public void setExp( Exp expr ) {
      this.expr = expr;
    }

    public void setName( String newName ) {
      throw new UnsupportedOperationException();
    }

    public Type getType() {
      return expr.getType();
    }

    public Map<String, Annotation> getAnnotationMap() {
      return Collections.emptyMap();
    }

    public NamedSet validate( Validator validator ) {
      Exp newExpr = expr.accept( validator );
      final Type type = newExpr.getType();
      if ( type instanceof MemberType || type instanceof TupleType ) {
        newExpr = new UnresolvedFunCall( "{}", Syntax.Braces, new Exp[] { newExpr } ).accept( validator );
      }
      this.expr = newExpr;
      return this;
    }

    public String getUniqueName() {
      return name;
    }

    public String getDescription() {
      throw new UnsupportedOperationException();
    }

    public OlapElement lookupChild( SchemaReader schemaReader, Id.Segment s, MatchType matchType ) {
      throw new UnsupportedOperationException();
    }

    public String getQualifiedName() {
      throw new UnsupportedOperationException();
    }

    public String getCaption() {
      throw new UnsupportedOperationException();
    }

    public boolean isVisible() {
      throw new UnsupportedOperationException();
    }

    public Hierarchy getHierarchy() {
      throw new UnsupportedOperationException();
    }

    public Dimension getDimension() {
      throw new UnsupportedOperationException();
    }

    public String getLocalized( LocalizedProperty prop, Locale locale ) {
      throw new UnsupportedOperationException();
    }
  }

  /**
   * Visitor that locates and registers parameters.
   */
  private class ParameterFinder extends MdxVisitorImpl {
    public Object visit( ParameterExpr parameterExpr ) {
      Parameter parameter = parameterExpr.getParameter();
      if ( !parameters.contains( parameter ) ) {
        parameters.add( parameter );
        parametersByName.put( parameter.getName(), parameter );
      }
      return null;
    }

    public Object visit( UnresolvedFunCall call ) {
      if ( call.getFunName().equals( "Parameter" ) ) {
        // Is there already a parameter with this name?
        String parameterName = ParameterFunDef.getParameterName( call.getArgs() );
        if ( parametersByName.get( parameterName ) != null ) {
          throw MondrianResource.instance().ParameterDefinedMoreThanOnce.ex( parameterName );
        }

        Type type = ParameterFunDef.getParameterType( call.getArgs() );

        // Create a temporary parameter. We don't know its
        // type yet. The default of NULL is temporary.
        Parameter parameter = new ParameterImpl( parameterName, Literal.nullValue, null, type );
        parameters.add( parameter );
        parametersByName.put( parameterName, parameter );
      }
      return null;
    }
  }

  /**
   * Visitor that locates and registers all aliased expressions ('expr AS alias') as named sets. The resulting named
   * sets have scope, therefore they can only be seen and used within that scope.
   */
  private class AliasedExpressionFinder extends MdxVisitorImpl {
    @Override
    public Object visit( QueryAxis queryAxis ) {
      registerAlias( queryAxis, queryAxis.getSet() );
      return super.visit( queryAxis );
    }

    public Object visit( UnresolvedFunCall call ) {
      registerAliasArgs( call );
      return super.visit( call );
    }

    public Object visit( ResolvedFunCall call ) {
      registerAliasArgs( call );
      return super.visit( call );
    }

    /**
     * Registers all arguments of a function that are named sets.
     *
     * @param call
     *          Function call
     */
    private void registerAliasArgs( FunCall call ) {
      for ( Exp exp : call.getArgs() ) {
        registerAlias( (QueryPart) call, exp );
      }
    }

    /**
     * Registers a named set if an expression is of the form "expr AS alias".
     *
     * @param parent
     *          Parent node
     * @param exp
     *          Expression that may be an "AS"
     */
    private void registerAlias( QueryPart parent, Exp exp ) {
      if ( exp instanceof FunCall ) {
        FunCall call2 = (FunCall) exp;
        if ( call2.getSyntax() == Syntax.Infix && call2.getFunName().equals( "AS" ) ) {
          // Scope is the function enclosing the 'AS' expression.
          // For example, in
          // Filter(Time.Children AS s, x > y)
          // the scope of the set 's' is the Filter function.
          assert call2.getArgCount() == 2;
          if ( call2.getArg( 1 ) instanceof Id ) {
            final Id id = (Id) call2.getArg( 1 );
            createScopedNamedSet( ( (Id.NameSegment) id.getSegments().get( 0 ) ).getName(), parent, call2.getArg(
                0 ) );
          } else if ( call2.getArg( 1 ) instanceof NamedSetExpr ) {
            NamedSetExpr set = (NamedSetExpr) call2.getArg( 1 );
            createScopedNamedSet( set.getNamedSet().getName(), parent, call2.getArg( 0 ) );
          }
        }
      }
    }
  }
}

// End Query.java
